package xyz.lwm.lazycat.mybatisflex;

import com.mybatisflex.core.BaseMapper;
import com.mybatisflex.core.FlexGlobalConfig;
import com.mybatisflex.core.MybatisFlexBootstrap;
import com.mybatisflex.core.mybatis.FlexConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.transaction.TransactionFactory;
import xyz.lwm.lazycat.annotation.Bean;
import xyz.lwm.lazycat.annotation.ConditionalOnBean;
import xyz.lwm.lazycat.annotation.ConditionalOnMissingBean;
import xyz.lwm.lazycat.annotation.Configuration;
import xyz.lwm.lazycat.beans.BeanDesign;
import xyz.lwm.lazycat.beans.BeanDesignBuilder;
import xyz.lwm.lazycat.beans.BeanDesignImportRegistrar;
import xyz.lwm.lazycat.beans.BeanDesignRegistry;
import xyz.lwm.lazycat.beans.Beans;
import xyz.lwm.lazycat.beans.BeansCustomizer;
import xyz.lwm.lazycat.beans.Dependency;
import xyz.lwm.lazycat.utility.CollectionUtil;
import xyz.lwm.lazycat.utility.It;
import xyz.lwm.lazycat.utility.StringUtil;
import xyz.lwm.lazycat.utility.reflect.Annotations;
import xyz.lwm.lazycat.utility.reflect.ClassUtil;
import xyz.lwm.lazycat.utility.resource.Resource;
import xyz.lwm.lazycat.utility.scan.PatternClassScanner;
import xyz.lwm.lazycat.utility.scan.ScanUtil;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * MybatisFlex配置
 *
 * @author lwm
 */
@Slf4j
@Configuration
@ConditionalOnBean(DataSource.class)
public class MybatisFlexConfiguration implements BeansCustomizer {

    private static final List<String> basePackages = new ArrayList<>();

    private static final List<String> xmlLocations = new ArrayList<>();

    private BeanDesignRegistry beanRegistry;

    private MybatisFlexBootstrap bootstrap;

    /**
     * 早于 bean 实例时执行
     *
     * @param registry bean 设计注册器
     * @param beans    bean 容器
     */
    @Override
    public void customize(BeanDesignRegistry registry, Beans beans) {
        this.beanRegistry = registry;
        this.bootstrap = MybatisFlexBootstrap.getInstance();

        // get properties
        MybatisFlexProperties flexProperties = beans.get(MybatisFlexProperties.class);
        // get base packages and xml locations
        addBasePackages(split(flexProperties.getMapperPackages()));
        addXmlLocations(split(flexProperties.getXmlLocations()));
        // register mapper designs
        log.info("register mybatis mappers");
        registerMapperDesigns(bootstrap);
    }

    /**
     * mybatis flex 配置
     */
    @Bean
    public MybatisFlexProperties mybatisFlexProperties() {
        return new MybatisFlexProperties();
    }

    /**
     * 创建事务工厂, 业务层可以自定义
     */
    @Bean
    @ConditionalOnMissingBean
    public TransactionFactory transactionFactory() {
        return new LazycatTransactionFactory();
    }

    /**
     * 创建 sqlSessionFactory
     *
     * @param dataSource         数据源
     * @param transactionFactory 事务工厂
     * @param customizers        配置定制
     * @param flexProperties     mybatis flex 属性
     * @return sqlSessionFactory
     */
    @Bean
    @ConditionalOnMissingBean
    @SuppressWarnings("unchecked")
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource,
                                               TransactionFactory transactionFactory,
                                               List<ConfigurationCustomizer> customizers,
                                               MybatisFlexProperties flexProperties) {

        log.info("MybatisFlex bootstrap start...");
        bootstrap.setDataSource(dataSource)
                .setTransactionFactory(transactionFactory);

        // environment and flex configuration
        Environment environment = new Environment(bootstrap.getEnvironmentId(),
                bootstrap.getTransactionFactory(), bootstrap.getDataSource());
        FlexConfiguration configuration = new FlexConfiguration(environment);

        // customize configuration
        customizers.forEach(c -> c.customize(configuration));
        bootstrap.setConfiguration(configuration);

        // set log impl
        It.of(flexProperties.getLogImpl()).filter(StringUtil::isNotBlank).let(ClassUtil::forName)
                .let(clz -> (Class<? extends Log>) clz).also(configuration::setLogImpl);

        // build sqlSessionFactory
        bootstrap.start();

        // load xml after bootstrap start, because it will register mappers, there are some conflicts
        log.info("load mybatis xml files");
        loadXmls(configuration);

        return FlexGlobalConfig.getConfig(environment.getId()).getSqlSessionFactory();
    }

    @SuppressWarnings("unchecked")
    private void registerMapperDesigns(MybatisFlexBootstrap bootstrap) {
        // scan mapper classes and add classes to mappers
        Set<Class<?>> classes = scanMapperClass();
        for (Class<?> clz : classes) {
            // add mapper class
            bootstrap.addMapper(clz);
            // register mapper design
            Dependency dependency = new Dependency(MybatisFlexConfiguration.class, "", SqlSessionFactory.class);
            BeanDesign<Object> design = new BeanDesignBuilder<>()
                    .type((Class<Object>) clz)
                    .name(StringUtil.lowercaseChar(clz.getSimpleName(), 0))
                    .instantiable((var1, var2) -> {
                        // apply dependency, 触发依赖, 确保 bean 加载顺序
                        var1[0].apply(var2);
                        return bootstrap.getMapper(clz);
                    }, dependency).build();
            beanRegistry.register(design);
        }
    }

    private Set<Class<?>> scanMapperClass() {
        Set<Class<?>> set = new HashSet<>();
        for (String pattern : basePackages) {
            if (StringUtil.isNotBlank(pattern)) {
                List<Class<?>> classes = new PatternClassScanner().scan(pattern,
                        clz -> BaseMapper.class.isAssignableFrom(clz) && clz.isInterface() &&
                                clz.getAnnotation(NoMapperBean.class) == null);
                set.addAll(classes);
            }
        }
        return set;
    }

    private String[] split(String str) {
        return StringUtil.isBlank(str) ? new String[0] : str.split("[,\\s]+");
    }


    // load and parse xml
    private void loadXmls(FlexConfiguration configuration) {
        Set<Resource> resources = scanXmResources();
        for (Resource r : resources) {
            try (InputStream in = r.getInputStream()) {
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(
                        in, configuration, r.getName(), configuration.getSqlFragments());
                xmlMapperBuilder.parse();
            } catch (IOException e) {
                //
            }
        }
    }

    private Set<Resource> scanXmResources() {
        List<Resource> list = new ArrayList<>();
        for (String xmlLocation : xmlLocations) {
            if (StringUtil.isNotBlank(xmlLocation)) {
                list.addAll(ScanUtil.scanResources(xmlLocation.trim()));
            }
        }
        Collection<Resource> values = list.stream()
                .collect(Collectors.toMap(Resource::getPath, r -> r)).values();
        return new HashSet<>(values);
    }

    /**
     * MybatisFlex扫描器
     *
     * @author lwm
     */
    public static class MybatisFlexScannerRegistrar implements BeanDesignImportRegistrar {

        @Override
        public void importBeanDesigns(Annotations annotations, BeanDesignRegistry beanRegistry) {
            MapperScan mapperScan = annotations.getAnnotation(MapperScan.class);
            if (mapperScan != null) {
                addBasePackages(mapperScan.basePackages());
                addXmlLocations(mapperScan.xmlLocations());
            }
        }

    }

    private static void addBasePackages(String[] basePackages) {
        if (CollectionUtil.isNotEmpty(basePackages)) {
            MybatisFlexConfiguration.basePackages.addAll(Arrays.asList(basePackages));
        }
    }

    private static void addXmlLocations(String[] xmlLocations) {
        if (CollectionUtil.isNotEmpty(xmlLocations)) {
            MybatisFlexConfiguration.xmlLocations.addAll(Arrays.asList(xmlLocations));
        }
    }

}
